import json
import re
from typing import Optional, Dict, List, TypeAlias

from fastapi import HTTPException, status
from kubernetes.client import (
    ApiClient,
    V1Deployment,
    V1StatefulSet,
    V1DaemonSet,
    V1ObjectMeta,
    V1Container,
    V1SecurityContext,
)
from loguru import logger
from urllib3 import HTTPResponse

from app.api.models import (
    Status,
    AdmissionResponse,
    AdmissionReviewRequest,
    AdmissionRequest,
    AdmissionReviewResponse,
    PatchOperation,
    Op,
)
from app.core.config import settings

FUNC_PARAMS = "func params"
FUNC_RESULT = "func result"
SUPPORT_KIND: TypeAlias = V1Deployment | V1StatefulSet | V1DaemonSet


class AdmissionWebhook(object):
    def __init__(self, ar: AdmissionReviewRequest):
        self.ar: AdmissionReviewRequest = ar
        self._request_obj: Optional[SUPPORT_KIND] = None

    @property
    def admission_request(self) -> AdmissionRequest:
        return self.ar.request

    @property
    def request_uid(self) -> str:
        return self.admission_request.uid

    @property
    def request_obj(self) -> SUPPORT_KIND:
        if not self._request_obj:
            self._request_obj = self.get_request_obj()
        return self._request_obj

    def get_request_obj(self) -> SUPPORT_KIND:
        kind = self.admission_request.kind.kind
        raw = json.dumps(self.admission_request.object)
        logger.debug(f"{FUNC_PARAMS}, {kind=}, {raw=}")
        resp = HTTPResponse(body=raw)
        obj: SUPPORT_KIND = ApiClient().deserialize(resp, response_type=f"V1{kind}")
        if obj.api_version is None:
            detail = "Deserialize failed"
            logger.error(f"{detail}, {kind=}, {raw=}")
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
        logger.debug(f"{FUNC_RESULT}, {obj=}")
        return obj

    @property
    def request_obj_metadata(self) -> V1ObjectMeta:
        return self.request_obj.metadata

    @property
    def request_obj_name(self) -> str:
        return self.request_obj_metadata.name

    @property
    def request_obj_namespace(self) -> str:
        return self.request_obj_metadata.namespace

    @property
    def request_obj_annotations(self) -> Dict[str, str]:
        if annotations := self.request_obj_metadata.annotations:
            return annotations
        return {}

    @property
    def request_obj_containers(self) -> List[V1Container]:
        return self.request_obj.spec.template.spec.containers

    def admission_required(self, annotation_key: str) -> bool:
        logger.debug(
            f"func_params, {annotation_key=}, metadata={self.request_obj_metadata}"
        )
        if self.request_obj_namespace in settings.IGNORE_NAMESPACES:
            logger.info(
                f"Ignore namespace, Skip..., "
                f"name={self.request_obj_name}, namespace={self.request_obj_namespace}, "
                f"ignore_namespaces={settings.IGNORE_NAMESPACES}"
            )
            return False
        annotation_value = self.request_obj_annotations.get(annotation_key.lower())
        if annotation_value in ["n", "no", "false", "off"]:
            logger.info(
                f"Ignore annotation, Skip..., "
                f"name={self.request_obj_name}, namespace={self.request_obj_namespace}, "
                f"{annotation_key=}, {annotation_value=}"
            )
            return False
        return True

    def validation_required(self) -> bool:
        required = self.admission_required(settings.ANNOTATION_VALIDATE_KEY)
        logger.debug(
            f"Validation admission required for "
            f"{self.request_obj_namespace}/{self.request_obj_name}: {required}"
        )
        return required

    def mutation_required(self) -> bool:
        if self.admission_required(settings.ANNOTATION_MUTATE_KEY):
            required = True
        else:
            _status = self.request_obj_annotations.get(
                settings.ANNOTATION_STATUS_KEY, ""
            ).lower()
            required = _status != "mutated"
        logger.debug(
            f"Mutation admission required for "
            f"{self.request_obj_namespace}/{self.request_obj_name}: {required}"
        )
        return required

    def build_error_response(
        self, code: int, err_msg: Optional[str] = None
    ) -> AdmissionResponse:
        return AdmissionResponse(
            uid=self.request_uid,
            allowed=False,
            status=Status(code=code, message=err_msg, reason=err_msg, status="Failure"),
        )

    @staticmethod
    def build_admission_review(response: AdmissionResponse) -> AdmissionReviewResponse:
        return AdmissionReviewResponse(
            apiVersion="admission.k8s.io/v1",
            kind="AdmissionReview",
            response=response
        )

    def validate_containers(self) -> AdmissionResponse:
        not_private_repo = "Image is not in private repo"
        run_as_privileged = "Run as privileged is not allowed"
        for container in self.request_obj_containers:
            container_name = container.name
            image = container.image
            if not image.startswith(settings.PRIVATE_REPO):
                logger.error(f"{not_private_repo}, {container_name=}, {image=}")
                return self.build_error_response(402, not_private_repo)

            sc: Optional[V1SecurityContext] = container.security_context
            if sc and sc.privileged:
                logger.error(f"{run_as_privileged}, {container_name=}, {sc=}")
                return self.build_error_response(403, run_as_privileged)
        return AdmissionResponse(uid=self.request_uid, allowed=True)

    def validate(self) -> AdmissionReviewResponse:
        if self.validation_required():
            response = self.validate_containers()
        else:
            response = AdmissionResponse(uid=self.request_uid, allowed=True)
        return self.build_admission_review(response)

    def mutate(self) -> AdmissionReviewResponse:
        allowed_response = AdmissionResponse(uid=self.request_uid, allowed=True)
        allowed_review = self.build_admission_review(allowed_response)

        if not self.mutation_required():
            return allowed_review

        annotations = self.request_obj_annotations
        sidecar_name = annotations.get(settings.ANNOTATION_SIDECAR_NAME_KEY)
        sidecar_image = annotations.get(settings.ANNOTATION_SIDECAR_IMAGE_KEY)

        if not all([sidecar_name, sidecar_image]):
            logger.info("Sidecar name or image is empty, skip mutating...")
            return allowed_review

        for container in self.request_obj_containers:
            if container.name == sidecar_name:
                logger.info("Sidecar already exists, skip mutating...")
                return allowed_review

        patch = self.create_patch()
        response = AdmissionResponse(
            uid=self.request_uid,
            allowed=True,
            patch=json.dumps([p.model_dump() for p in patch]).encode(),
            patchType="JSONPatch",
        )
        review = self.build_admission_review(response)
        logger.info(f"{FUNC_RESULT}, {review=}")
        return review

    def create_patch(self) -> List[PatchOperation]:
        annotations = self.request_obj_annotations
        logger.debug(f"{FUNC_PARAMS}, {annotations=}")
        added = {settings.ANNOTATION_STATUS_KEY: "mutated"}
        patch = self.update_annotation(added) + self.add_sidecar()
        logger.debug(f"{FUNC_RESULT}, {patch=}")
        return patch

    def update_annotation(self, added: Dict[str, str]) -> List[PatchOperation]:
        origin_annotations = self.request_obj_annotations
        patch = []
        logger.debug(f"{FUNC_PARAMS}, {origin_annotations=}, {added=}")
        for key, value in added.items():
            if key in origin_annotations:
                op = Op.replace_
            else:
                op = Op.add
            patch.append(
                PatchOperation(
                    op=op,
                    path=f"/metadata/annotations/{self.escape_json(key)}",
                    value=value,
                )
            )
        logger.debug(f"{FUNC_PARAMS}, {patch=}")
        return patch

    @staticmethod
    def escape_json(s: str) -> str:
        s = s.replace("~", "~0")
        s = s.replace("/", "~1")
        return s

    def add_sidecar(self) -> List[PatchOperation]:
        annotations = self.request_obj_annotations
        sidecar_name = annotations.get(settings.ANNOTATION_SIDECAR_NAME_KEY)
        sidecar_image = annotations.get(settings.ANNOTATION_SIDECAR_IMAGE_KEY)
        if origin_sidecar_command := annotations.get(
            settings.ANNOTATION_SIDECAR_COMMAND_KEY
        ):
            sidecar_command = re.split(r"\s+", origin_sidecar_command)
        else:
            sidecar_command = []
        logger.debug(
            f"{FUNC_PARAMS}, {sidecar_name=}, {sidecar_image=}, "
            f"{sidecar_command=}, {origin_sidecar_command=}"
        )
        sidecar = V1Container(
            name=sidecar_name,
            image=sidecar_image,
            command=sidecar_command,
        )
        return [
            PatchOperation(
                op=Op.add,
                path="/spec/template/spec/containers/0",
                value=sidecar.to_dict(),
            )
        ]
